package com.mq.core;

import com.mq.core.endpoint.RedisListenerEndpoint;
import com.mq.core.factory.RedisListenerContainerFactory;
import org.apache.commons.lang.StringUtils;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * huj
 */
public class RedisListenerAnnotationBeanPostProcessor
        implements BeanPostProcessor, Ordered, BeanFactoryAware, SmartInitializingSingleton {

    private BeanFactory beanFactory;

    private RedisListenerEndpointRegistrar registrar = new RedisListenerEndpointRegistrar();

    private boolean isStarted;

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public void afterSingletonsInstantiated() {
        if(!isStarted){
            registrar.startContainer();
        }
        isStarted = true;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        Class<?> targetClass = AopUtils.getTargetClass(bean);
        final TypeMetadata metadata = buildMetadata(targetClass);
        for(ListenerMethod listenerMethod : metadata.listenerMethods){
            processRedisListener(listenerMethod.annotation, listenerMethod.method, bean, beanName);
        }
        return bean;
    }

    private void processRedisListener(RedisListener redisListener, Method method, Object bean, String beanName){
        Method methodToUse = checkProxy(method, bean);
        RedisListenerEndpoint endpoint = new RedisListenerEndpoint();
        endpoint.setMethod(methodToUse);
        endpoint.setBean(bean);
        endpoint.setQueueName(redisListener.queues());
        endpoint.setId(methodToUse.getDeclaringClass().getName()+"#"+methodToUse.getName());
        //容器工厂
        RedisListenerContainerFactory<?> factory;
        String containerFactoryBeanName = redisListener.containerFactory();
        if(StringUtils.isNotBlank(containerFactoryBeanName)){
            Assert.state(this.beanFactory != null, "BeanFactory must be set to obtain container factory by bean name");
            try {
                factory = (RedisListenerContainerFactory)this.beanFactory
                        .getBean(containerFactoryBeanName, RedisListenerContainerFactory.class);
            }catch (BeansException ex) {
                throw new BeanInitializationException("没有找到containerFactoryBean="+containerFactoryBeanName, ex);
            }
        }else{
            //默认需要配置一个 redisListenerContainerFactory
            try {
                factory = (RedisListenerContainerFactory)this.beanFactory
                        .getBean("redisListenerContainerFactory", RedisListenerContainerFactory.class);
            }catch (BeansException ex) {
                throw new BeanInitializationException("没有找到containerFactoryBean=redisListenerContainerFactory", ex);
            }
        }
        endpoint.setRedisTemplate(redisTemplate);
        registrar.registerEndpoint(endpoint, factory);
    }

    private Method checkProxy(Method method, Object bean) {
        if (AopUtils.isJdkDynamicProxy(bean)) {
            try {
                method = bean.getClass().getMethod(method.getName(), method.getParameterTypes());
                Class<?>[] proxiedInterfaces = ((Advised) bean).getProxiedInterfaces();
                for (Class<?> iface : proxiedInterfaces) {
                    try {
                        method = iface.getMethod(method.getName(), method.getParameterTypes());
                        break;
                    }
                    catch (NoSuchMethodException noMethod) {
                    }
                }
            }
            catch (SecurityException ex) {
                ReflectionUtils.handleReflectionException(ex);
            }catch (NoSuchMethodException ex) {
                throw new IllegalStateException(String.format(
                        "@RedisListener method '%s' found on bean target class '%s', " +
                                "but not found in any interface(s) for bean JDK proxy. Either " +
                                "pull the method up to an interface or switch to subclass (CGLIB) " +
                                "proxies by setting proxy-target-class/proxyTargetClass " +
                                "attribute to 'true'", method.getName(), method.getDeclaringClass().getSimpleName()));
            }
        }
        return method;
    }

    private TypeMetadata buildMetadata(Class<?> targetClass) {
        final List<ListenerMethod> listenerMethods = new ArrayList<>();
        Method[] methods = targetClass.getDeclaredMethods();
        if(methods != null && methods.length > 0){
            for(Method method : methods){
                RedisListener redisListener = AnnotationUtils.findAnnotation(method, RedisListener.class);
                if(redisListener != null){
                    listenerMethods.add(new ListenerMethod(method, redisListener));
                }
            }
        }
        return new TypeMetadata(listenerMethods
                .toArray(new ListenerMethod[listenerMethods.size()]));
    }

    public RedisListenerEndpointRegistrar getRegistrar() {
        return registrar;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }

    private static class ListenerMethod {

        final Method method;

        final RedisListener annotation;

        ListenerMethod(Method method, RedisListener annotation) { // NOSONAR
            this.method = method;
            this.annotation = annotation;
        }

    }

    private static class TypeMetadata {

        final ListenerMethod[] listenerMethods;

        static final TypeMetadata EMPTY = new TypeMetadata();

        private TypeMetadata() {
            this.listenerMethods = new ListenerMethod[0];
        }

        TypeMetadata(ListenerMethod[] methods) { // NOSONAR
            this.listenerMethods = methods;
        }

    }
}
